package org.jpropelleralt.node.impl;

import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.box.BoxFeatures;
import org.jpropelleralt.box.impl.BoxFeaturesDefault;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.change.ChangeDefault;
import org.jpropelleralt.multiverse.Multiverse;
import org.jpropelleralt.node.Node;
import org.jpropelleralt.reaction.Reaction;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.universe.Universe;

/**
 * Default implementation of {@link Node}
 */
public class NodeDefault implements Node {

	private final BoxFeatures features;
	private final NodeReaction reaction;
	private final Map<String, Ref<?>> refs;
	private final List<String> refNames;
	private final List<String> refNamesUnmodifiable;

	/**
	 * Create a {@link NodeDefault} in the {@link Universe}
	 * returned by {@link Multiverse#getCurrentUniverse()}
	 */
	protected NodeDefault() {
		this(Multiverse.getCurrentUniverse());
	}

	/**
	 * Create a {@link NodeDefault}
	 * @param universe		The {@link Universe} in which 
	 * 						the {@link NodeDefault} will exist.
	 */
	protected NodeDefault(Universe universe) {
		this.features = new BoxFeaturesDefault(this, universe);
		this.reaction = new NodeReaction();
		this.refs = new HashMap<String, Ref<?>>();
		this.refNames = new LinkedList<String>();
		this.refNamesUnmodifiable = Collections.unmodifiableList(this.refNames);
	}
	
	@Override
	public Ref<?> get(String name) {
		return refs.get(name);
	}

	@Override
	public BoxFeatures features() {
		return features;
	}

	@Override
	public Iterator<String> iterator() {
		return refNamesUnmodifiable.iterator();
	}

	/**
	 * Add another {@link Ref} to the {@link Node}
	 * @param name	The name for the {@link Ref}. If this name is already used,
	 * 				an {@link IllegalArgumentException} will be thrown.
	 * @param ref	The {@link Ref} to add.
	 */
	protected <T> Ref<T> addRef(String name, Ref<T> ref) {
		//FIXME - Should we have a contract that
		//refs are ONLY added before the Node becomes available to anything outside its own constructor?
		Multiverse.prepareWrite(this);
		try {
			if (refs.containsKey(name)) {
				throw new IllegalArgumentException("Node already contains a Ref for name '" + name + "'");
			}
			refs.put(name, ref);
			ref.features().addReaction(reaction);
			refNames.add(name);
			
			//Adding a ref counts as a change
			Multiverse.propagateWrite(this, ChangeDefault.shallowInstance());
			
			return ref;
		} finally {
			Multiverse.concludeWrite(this);
		}
	}
	
	/**
	 * A {@link Reaction} that just makes sure the {@link NodeDefault}
	 * changes when its contents do.
	 */
	private class NodeReaction implements Reaction {

		Map<Box, Change> defaultChanges = new IdentityHashMap<Box, Change>();
		
		private NodeReaction() {
			defaultChanges.put(NodeDefault.this, ChangeDefault.deepInstance());
		}
		
		@Override
		public void apply(boolean propagate) {
			//Nothing to do when we apply - we just propagate changes
		}
		@Override
		public boolean completeChanges(Map<Box, Change> changes) {
			//Nothing to do
			return false;
		}

		@Override
		public Map<Box, Change> react(Map<Box, Change> changes) {
			//We only listen to values of the NodeDefault, so
			//if we see any change, it indicates a deep change to
			//NodeDefault
			return defaultChanges;
		}
	}
}
